import { GlLink, GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import { EMPTY_BODY_MESSAGE } from 'ee/vue_shared/security_reports/components/constants';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import VulnerabilityDetails from 'ee/vue_shared/security_reports/components/vulnerability_details.vue';
import FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue';
import GenericReportSection from 'ee/vulnerabilities/components/generic_report/report_section.vue';
import VulnerabilityTraining from 'ee/vulnerabilities/components/vulnerability_training.vue';
import {
  SUPPORTING_MESSAGE_TYPES,
  SUPPORTED_IDENTIFIER_TYPE_CWE,
} from 'ee/vulnerabilities/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { mockFindings } from '../mock_data';

function makeVulnerability(changes = {}) {
  return Object.assign(cloneDeep(mockFindings[0]), changes);
}

describe('VulnerabilityDetails component', () => {
  let wrapper;

  const componentFactory = (vulnerability, provide = {}) => {
    wrapper = mountExtended(VulnerabilityDetails, {
      propsData: { vulnerability },
      provide,
      stubs: {
        VulnerabilityTraining: true,
      },
    });
  };

  const expectSafeLink = ({ link, href, text, isExternal = true }) => {
    expect(link.is(GlLink)).toBe(true);
    expect(link.attributes('href')).toBe(href);
    expect(link.text()).toBe(text);
    if (isExternal) {
      expect(link.attributes('rel')).toContain('noopener noreferrer');
    }
  };

  const findLink = (name) => wrapper.findComponent({ ref: `${name}Link` });
  const findRequest = () => wrapper.findComponent({ ref: 'request' });
  const findRecordedResponse = () => wrapper.findComponent({ ref: 'recordedResponse' });
  const findUnmodifiedResponse = () => wrapper.findComponent({ ref: 'unmodifiedResponse' });
  const findCrashAddress = () => wrapper.findComponent({ ref: 'crashAddress' });
  const findCrashState = () => wrapper.findComponent({ ref: 'crashState' });
  const findCrashType = () => wrapper.findComponent({ ref: 'crashType' });
  const findStacktraceSnippet = () => wrapper.findComponent({ ref: 'stacktraceSnippet' });
  const findGenericReportSection = () => wrapper.findComponent(GenericReportSection);
  const findAlert = () => wrapper.findComponent(FalsePositiveAlert);
  const findDescription = () => wrapper.findByTestId('description');
  const findTraining = () => wrapper.findComponent(VulnerabilityTraining);

  const USER_NOT_FOUND_MESSAGE = '{"message":"User not found."}';

  it('renders false positive alert', () => {
    const vulnerability = makeVulnerability({ false_positive: true });
    componentFactory(vulnerability, {
      canViewFalsePositive: true,
    });

    expect(findAlert().exists()).toBe(true);
  });

  it('renders description text when markdown is not present', () => {
    const vulnerability = makeVulnerability();
    componentFactory(vulnerability);

    const section = findDescription();
    expect(section.text()).toBe(vulnerability.description);
  });

  it('renders description html when markdown is present', () => {
    const vulnerability = makeVulnerability({ description_html: '<code>test</code>' });
    componentFactory(vulnerability);

    const section = findDescription();
    expect(section.html()).toContain(vulnerability.description_html);
  });

  it('renders severity with a badge', () => {
    const vulnerability = makeVulnerability({ severity: 'critical' });
    componentFactory(vulnerability);
    const badge = wrapper.findComponent(SeverityBadge);

    expect(badge.props('severity')).toBe(vulnerability.severity);
  });

  it('renders status with a badge', () => {
    const vulnerability = makeVulnerability({ state: 'detected' });
    componentFactory(vulnerability);
    const badge = wrapper.findComponent(GlBadge);

    expect(badge.text()).toContain(vulnerability.state);
  });

  it('renders link fields with link', () => {
    const vulnerability = makeVulnerability();
    componentFactory(vulnerability);

    expectSafeLink({
      link: findLink('project'),
      href: vulnerability.project.full_path,
      text: vulnerability.project.full_name,
      isExternal: false,
    });
  });

  it('renders wrapped file paths', () => {
    const vulnerability = makeVulnerability({
      blob_path: `${TEST_HOST}/bar`,
      location: {
        file: '/some/file/path',
      },
    });

    componentFactory(vulnerability);

    expect(findLink('file').html()).toMatch('/<wbr>some/<wbr>file/<wbr>path');
  });

  it('escapes wrapped file paths', () => {
    const vulnerability = makeVulnerability({
      blob_path: `${TEST_HOST}/bar`,
      location: {
        file: '/unsafe/path<script></script>',
      },
    });

    componentFactory(vulnerability);

    expect(findLink('file').html()).toMatch(
      '/<wbr>unsafe/<wbr>path&lt;script&gt;&lt;/<wbr>script&gt;',
    );
  });

  describe('does not render XSS links', () => {
    // eslint-disable-next-line no-script-url
    const badUrl = 'javascript:alert("")';

    beforeEach(() => {
      const vulnerability = makeVulnerability({
        blob_path: badUrl,
        location: {
          file: 'badFile.lock',
        },
        links: [{ url: badUrl }],
        identifiers: [{ name: 'BAD_URL', url: badUrl }],
        assets: [{ name: 'BAD_URL', url: badUrl }],
      });

      componentFactory(vulnerability);
    });

    it('for the link field', () => {
      expectSafeLink({ link: findLink('links'), href: 'about:blank', text: badUrl });
    });

    it('for the identifiers field', () => {
      expectSafeLink({ link: findLink('identifiers'), href: 'about:blank', text: 'BAD_URL' });
    });

    it('for the assets field', () => {
      expectSafeLink({ link: findLink('assets'), href: 'about:blank', text: 'Download BAD_URL' });
    });

    it('for the file field', () => {
      expectSafeLink({
        link: findLink('file'),
        href: 'about:blank',
        text: 'badFile.lock',
        isExternal: false,
      });
    });
  });

  describe('with coverage fuzzing information', () => {
    let vulnerability;
    beforeEach(() => {
      vulnerability = makeVulnerability({
        location: {
          crash_address: '0x602000001573',
          crash_state: 'FuzzMe\nstart\nstart+0x0\n\n',
          crash_type: 'Heap-buffer-overflow\nREAD 1',
          stacktrace_snippet: 'test snippet',
        },
      });
      componentFactory(vulnerability);
    });

    it('renders crash_address', () => {
      expect(findCrashAddress().exists()).toBe(true);
      expect(findStacktraceSnippet().exists()).toBe(true);
    });

    it('renders crash_state', () => {
      expect(findCrashState().exists()).toBe(true);
      expect(findCrashState().html()).toContain(vulnerability.location.crash_state);
    });

    it('renders crash_type', () => {
      expect(findCrashType().exists()).toBe(true);
      expect(findCrashType().text()).toContain(vulnerability.location.crash_type);
    });
  });

  describe.each([
    ['', ''],
    [undefined, EMPTY_BODY_MESSAGE],
    [null, EMPTY_BODY_MESSAGE],
    [USER_NOT_FOUND_MESSAGE, USER_NOT_FOUND_MESSAGE],
  ])('with request information and body set to: %s', (body) => {
    let vulnerability;

    beforeEach(() => {
      vulnerability = makeVulnerability({
        request: {
          method: 'GET',
          url: 'http://foo.bar/path',
          headers: [
            { name: 'key1', value: 'value1' },
            { name: 'key2', value: 'value2' },
          ],
          body,
        },
      });
      componentFactory(vulnerability);
    });

    it('renders the request-url', () => {
      expect(findLink('url').attributes('href')).toBe('http://foo.bar/path');
    });
  });

  describe('without request information', () => {
    beforeEach(() => {
      const vulnerability = makeVulnerability({
        location: {
          hostname: 'http://foo.com',
          path: '/bar',
        },
      });
      componentFactory(vulnerability);
    });

    it('renders the location-url', () => {
      expect(findLink('url').text()).toBe('http://foo.com/bar');
    });

    it('does not render a code block containing the request', () => {
      expect(findRequest().exists()).toBe(false);
    });
  });

  describe.each([
    ['', ''],
    [undefined, EMPTY_BODY_MESSAGE],
    [null, EMPTY_BODY_MESSAGE],
    [USER_NOT_FOUND_MESSAGE, USER_NOT_FOUND_MESSAGE],
  ])('with response information and body set to: %s', (body) => {
    let vulnerability;

    beforeEach(() => {
      vulnerability = makeVulnerability({
        response: {
          status_code: '200',
          reason_phrase: 'INTERNAL SERVER ERROR',
          headers: [
            { name: 'key1', value: 'value1' },
            { name: 'key2', value: 'value2' },
          ],
          body,
        },
      });
      componentFactory(vulnerability);
    });

    it('renders the response status code', () => {
      expect(findUnmodifiedResponse().text()).toContain('200');
    });
  });

  describe('without unmodified response information', () => {
    beforeEach(() => {
      const vulnerability = makeVulnerability();
      componentFactory(vulnerability);
    });

    it('does not render the response', () => {
      expect(findUnmodifiedResponse().exists()).toBe(false);
    });
  });

  describe.each([
    ['', ''],
    [undefined, EMPTY_BODY_MESSAGE],
    [null, EMPTY_BODY_MESSAGE],
    [USER_NOT_FOUND_MESSAGE, USER_NOT_FOUND_MESSAGE],
  ])('with recorded response information and body set to: %s', (body) => {
    let vulnerability;

    beforeEach(() => {
      vulnerability = makeVulnerability({
        supporting_messages: [
          {
            name: SUPPORTING_MESSAGE_TYPES.RECORDED,
            response: {
              status_code: '200',
              reason_phrase: 'INTERNAL SERVER ERROR',
              headers: [
                { name: 'key1', value: 'value1' },
                { name: 'key2', value: 'value2' },
              ],
              body,
            },
          },
        ],
      });
      componentFactory(vulnerability);
    });

    it('renders the recorded response status code', () => {
      expect(findRecordedResponse().text()).toContain('200');
    });
  });

  describe('without response information', () => {
    beforeEach(() => {
      const vulnerability = makeVulnerability();
      componentFactory(vulnerability);
    });

    it('does not render the response', () => {
      expect(findRecordedResponse().exists()).toBe(false);
    });
  });

  describe('scanner details', () => {
    describe('with additional information', () => {
      beforeEach(() => {
        const vulnerability = makeVulnerability();
        componentFactory(vulnerability);
      });

      it('should include version information', () => {
        expect(findLink('scanner').text()).toBe('Gemnasium (version 1.1.1)');
      });

      it('should render link', () => {
        expect(findLink('scanner').find('a').exists()).toBe(true);
      });
    });

    describe('without additional information', () => {
      beforeEach(() => {
        const vulnerability = makeVulnerability({
          scanner: {
            id: 'trivy',
            name: 'Trivy',
          },
        });
        componentFactory(vulnerability);
      });

      it('should not render the link', () => {
        expect(findLink('scanner').exists()).toBe(false);
      });
    });
  });

  describe('generic report section', () => {
    describe('with vulnerability details data', () => {
      const details = { reportEntry: { type: 'foo', value: 'bar' } };

      beforeEach(() => {
        const vulnerability = makeVulnerability({ details });
        componentFactory(vulnerability);
      });

      it('renders the generic report section', () => {
        expect(findGenericReportSection().exists()).toBe(true);
      });

      it('passes the details data as a prop to the generic report section', () => {
        expect(findGenericReportSection().props('details')).toBe(details);
      });
    });

    describe('without vulnerability details data', () => {
      beforeEach(() => {
        const vulnerability = makeVulnerability({ details: null });
        componentFactory(vulnerability);
      });

      it('does not render the generic report section', () => {
        expect(findGenericReportSection().exists()).toBe(false);
      });
    });
  });

  describe('vulnerability training', () => {
    describe('with vulnerability identifiers', () => {
      const identifiers = [{ externalType: SUPPORTED_IDENTIFIER_TYPE_CWE, externalId: 'cwe-123' }];
      const projectFullPathLeadingSlash = '/namespace/project';
      const projectFullPathWithoutLeadingSlash = 'namespace/project';
      const project = {
        id: 7071551,
        name: 'project',
        full_path: projectFullPathLeadingSlash,
        full_name: 'GitLab.org / gitlab-ui',
      };

      beforeEach(() => {
        const vulnerability = makeVulnerability({ identifiers, project });
        componentFactory(vulnerability);
      });

      it('passes the correct props to the training section', () => {
        expect(findTraining().props()).toMatchObject({
          identifiers,
          projectFullPath: projectFullPathWithoutLeadingSlash,
          file: mockFindings[0].location.file,
        });
      });
    });

    describe('without vulnerability identifiers', () => {
      beforeEach(() => {
        const vulnerability = makeVulnerability({ identifiers: [] });
        componentFactory(vulnerability);
      });

      it('does not render the vulnerability training section', () => {
        expect(findTraining().exists()).toBe(false);
      });
    });
  });

  describe('pin test', () => {
    const factory = (vulnFinding) => {
      wrapper = shallowMount(VulnerabilityDetails, {
        propsData: {
          vulnerability: vulnFinding,
        },
      });
    };

    it('renders correctly', () => {
      factory(
        makeVulnerability({
          request: {
            url: 'http://foo.bar/path',
            headers: [
              { name: 'key1', value: 'value1' },
              { name: 'key2', value: 'value2' },
            ],
          },
          response: {
            status_code: '200',
            headers: [
              { name: 'key1', value: 'value1' },
              { name: 'key2', value: 'value2' },
            ],
          },
        }),
      );

      expect(wrapper.element).toMatchSnapshot();
    });
  });
});
